package org.zjvis.graph.analysis.service;

import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.fastjson.JSONObject;
import org.apache.tinkerpop.gremlin.structure.Vertex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.zjvis.datascience.common.graph.vo.LinkVO;
import org.zjvis.datascience.common.graph.vo.NodeVO;
import org.zjvis.graph.analysis.service.dto.GraphActionDTO;
import org.zjvis.graph.analysis.service.enums.ActionEnum;
import org.zjvis.graph.analysis.service.mapper.GraphActionMapper;
import org.zjvis.graph.analysis.service.model.util.GraphActionDeStack;
import org.zjvis.graph.analysis.service.vo.GraphActionVO;
import org.zjvis.graph.analysis.service.vo.GraphVO;

import javax.annotation.PreDestroy;
import java.util.*;
import java.util.stream.Collectors;

@Service
public class GraphActionService {

    private final static Logger logger = LoggerFactory.getLogger("GraphActionService");


    @Autowired
    private GraphActionMapper graphActionMapper;

    @Autowired
    private GraphActionStackWrapper graphActionStackWrapper;

    @Autowired
    private JanusGraphEmbedService janusGraphEmbedService;

    @Autowired
    private GraphAnalysisService graphAnalysisService;

    public boolean save(GraphActionDTO graphActionDTO) {
        return graphActionMapper.save(graphActionDTO);
    }

    public void delete(Long id) {
        graphActionMapper.delete(id);
    }

    public void deleteByGraphId(Long graphId) {
        graphActionMapper.deleteByGraphId(graphId);
    }

    public boolean update(GraphActionDTO graphActionDTO) {
        return graphActionMapper.update(graphActionDTO);
    }

    public GraphActionDTO queryById(Long id) {
        return graphActionMapper.queryById(id);
    }

    public List<GraphActionDTO> queryBygraphId(Long graphId) {
        return graphActionMapper.queryByGraphId(graphId);
    }

    public List<GraphActionDTO> queryLatestActionByGraphId(Long graphId, Long limitNum) {
        return graphActionMapper.queryLatestActionByGraphId(graphId, limitNum);
    }

    public boolean initStackForGraphId(Long graphId) {
        return graphActionStackWrapper.initStackForProjectId(graphId, this);
    }

    /**
     * 添加action
     * @param actionType 操作类型
     * @param ctx1 操作前现场
     * @param ctx2 操作后现场
     * @return
     */
    public boolean addActionForGraph(ActionEnum actionType, GraphVO ctx1, GraphVO ctx2) {
        return this.addActionForGraph(actionType, ctx1, ctx2, null);
    }

    public boolean addActionForGraph(ActionEnum actionType, GraphVO ctx1, GraphVO ctx2, GraphVO ctx3) {
        return addActionForGraph(actionType, ctx1, ctx2, ctx3, new JSONObject());
    }

    public boolean addActionForGraph(ActionEnum actionType, GraphVO ctx1, GraphVO ctx2, GraphVO ctx3, JSONObject context) {
        if (ctx1 == null) {
            return false;
        }
        this.initStackForGraphId(ctx1.getId());
        GraphActionDTO actionDTO = new GraphActionDTO();
        actionDTO.setActionType(actionType.label());
        actionDTO.setProjectId(ctx1.getProjectId());
        actionDTO.setGraphId(ctx1.getId());
        actionDTO.setUserId(ctx1.getUserId());
        JSONObject data = new JSONObject();
        data.put("ctx1", ctx1);
        data.put("ctx2", ctx2);
        data.put("ctx3", ctx3);
        context.put("data", data);
        actionDTO.setContext(context.toJSONString());
        boolean flag = this.save(actionDTO);
        GraphActionDeStack actionStack = graphActionStackWrapper
                .getActionDeStackByGraphId(ctx1.getId());
        actionStack.addUndoStack(actionDTO);
        compressedStack(actionStack);
        clearRedoStack(actionStack);
        return flag;
    }

    public GraphActionDTO undo(Long graphId) {
        this.initStackForGraphId(graphId);
        GraphActionDeStack actionDeStack = graphActionStackWrapper.getActionDeStackByGraphId(graphId);
        GraphActionDTO undoAction = actionDeStack.undo();
        if (undoAction == null) {
            logger.error("undo empty!!!");
            return null;
        }
        boolean flag = this.recoverHandler(undoAction, true);
        if (!flag) {
            logger.error("recover fail!!!");
            actionDeStack.redo();
        }
        return undoAction;
    }

    public GraphActionDTO redo(Long graphId) {
        this.initStackForGraphId(graphId);
        GraphActionDeStack actionDeStack = graphActionStackWrapper.getActionDeStackByGraphId(graphId);
        GraphActionDTO redoAction = actionDeStack.redo();
        if (redoAction == null) {
            logger.error("redo empty!!!");
            return null;
        }
        boolean flag = this.recoverHandler(redoAction, false);
        if (!flag) {
            logger.error("recover fail!!!");
            actionDeStack.undo();
        }
        return redoAction;
    }

    /**
     * redo和undo 实际操作入口，根据不同的actionType执行不通的恢复处理逻辑
     */
    public boolean recoverHandler(GraphActionDTO action, boolean isUndo) {
        if (action == null) {
            return false;
        }
        String actionType = action.getActionType();
        boolean flag = false;
        if (actionType.equals(ActionEnum.UPDATE.label())) {
            flag = this.updateHandler(action, isUndo);
        } else if (actionType.equals(ActionEnum.ADD.label())) {
            flag = this.addHandler(action, isUndo);
        } else if (actionType.equals(ActionEnum.DELETE.label())) {
            flag = this.deleteHandler(action, isUndo);
        } else if (actionType.equals(ActionEnum.GRAPH_FILTER.label())) {
            flag = this.graphFilterHandler(action, isUndo);
        }
        if (flag) {
            if (isUndo) {
                // if undo, then delete action from action table
                if (this.queryById(action.getId()) != null) {
                    try {
                        this.delete(action.getId());
                    } catch (Exception e) {
                        logger.error(e.getMessage());
                        return false;
                    }
                }
            } else {
                //重做，则将action加回到撤销表中
                save(action);
            }
        }
        return flag;
    }

    public boolean addHandler(GraphActionDTO action, boolean isUndo) {
        GraphActionVO actionVO = action.view();
        JSONObject context = actionVO.getContextObj();

        if (isUndo) {
            // 逆向操作删除
            this.batchDeleteContext(context);
        } else {
            // 重做添加
            this.batchAddContext(context);
        }
        action.setContext(context.toJSONString());
        return true;
    }

    public boolean deleteHandler(GraphActionDTO action, boolean isUndo) {
        GraphActionVO actionVO = action.view();
        JSONObject context = actionVO.getContextObj();

        if (isUndo) {
            // 逆向操作添加
            this.batchAddContext(context);
        } else {
            // 重做删除
            this.batchDeleteContext(context);
        }
        action.setContext(context.toJSONString());
        return true;
    }

    public boolean updateHandler(GraphActionDTO action, boolean isUndo) {
        GraphActionVO actionVO = action.view();
        JSONObject context = actionVO.getContextObj();
        JSONObject data = context.getJSONObject("data");
        GraphVO ctx1 = data.getObject("ctx1", GraphVO.class);
        GraphVO ctx2 = data.getObject("ctx2", GraphVO.class);
        GraphVO ctx;
        if (isUndo) {
            //逆向恢复成ctx1
            ctx = ctx1;
        } else {
            //重做成ctx2
            ctx = ctx2;
        }
        List<NodeVO> nodes = ctx.getNodes();
        List<LinkVO> links = ctx.getLinks();

        if (nodes.size() != 0 || links.size() != 0 || ctx.getActionContext() != null) {
            graphAnalysisService.batchUpdate(ctx.getId(), nodes, links, null, null, null, ctx.getActionContext());
        }
        return true;
    }

    public void batchDeleteContext(JSONObject context) {
        JSONObject data = context.getJSONObject("data");
        GraphVO graphVO = data.getObject("ctx1", GraphVO.class);
        List<String> nids = graphVO.getNodes().stream().map(NodeVO::getId).collect(Collectors.toList());
        List<String> lids = graphVO.getLinks().stream().map(LinkVO::getId).collect(Collectors.toList());
        if (nids.size() != 0 || lids.size() != 0) {
            graphAnalysisService.batchDeleteNodesAndLinks(graphVO.getId(), nids, lids, null);
        }
    }

    public void batchAddContext(JSONObject context) {
        JSONObject data = context.getJSONObject("data");
        GraphVO graphVO = data.getObject("ctx1", GraphVO.class);
        List<NodeVO> nodes = graphVO.getNodes();
        List<LinkVO> links = graphVO.getLinks();
        Long graphId = graphVO.getId();
        if (nodes.size() != 0 || links.size() != 0) {
            Map<String, String> idMapUpdate = new HashMap();
            for (NodeVO node: nodes) {
                Vertex v = janusGraphEmbedService.createVertex(node, node.getCategoryId(), graphId);
                idMapUpdate.put(v.id().toString(), node.getId());
            }
            for (LinkVO link: links) {
                janusGraphEmbedService.createEdge(null, null, link, link.getEdgeId(), graphId);
            }
            janusGraphEmbedService.commit();
            graphAnalysisService.batchUpdate(graphId, nodes, links, null, null, null, null, idMapUpdate);
        }
    }

    /**
     * 栈中的条数超过限制，删除
     */
    public void compressedStack(GraphActionDeStack actionStack) {
        int out = actionStack.outLimit();
        if (out > 0) {
            for (int i = 0; i < out; i++) {
                GraphActionDTO actionDTO = actionStack.removeUndoStack();
                this.delete(actionDTO.getId());
            }
        }
    }

    /**
     * 清空重做栈
     */
    public void clearRedoStack(GraphActionDeStack actionStack) {
        while (actionStack.redoStackSize() > 0) {
            GraphActionDTO redoAction = actionStack.removeRedoStack();
        }
    }

    /**
     * 在系统停止前清空redo栈,并删除表
     */
    @PreDestroy
    public void destroyRedoStack() {
        logger.info("system stopping... clear graph redoStack");
        Map<Long, GraphActionDeStack> stackWrapper = graphActionStackWrapper.getStackWrapper();
        Set<Long> keys = stackWrapper.keySet();
        if (CollectionUtil.isEmpty(keys)) {
            return;
        }
        for (Long key : keys) {
            GraphActionDeStack remove = stackWrapper.remove(key);
            clearRedoStack(remove);
        }
    }

    public JSONObject actionElementAna(GraphActionDTO graphActionDTO, boolean isUndo) {
        //适用于图分析仅有update和delete操作
        //适用于图分析过滤器应用操作
        JSONObject obj = new JSONObject();
        Long gid = graphActionDTO.getGraphId();
        String actionType = graphActionDTO.getActionType();
        obj.put("actionType", actionType);
        JSONObject context = JSONObject.parseObject(graphActionDTO.getContext());
        JSONObject contextData = context.getJSONObject("data");
        JSONObject ctx;
        if (actionType.equals(ActionEnum.DELETE.label())) {
            ctx = contextData.getJSONObject("ctx1");
        }
        else if (actionType.equals(ActionEnum.GRAPH_FILTER.label())) {
            return obj;
        }
        else {
            if (isUndo) {
                ctx = contextData.getJSONObject("ctx1");
            } else {
                ctx = contextData.getJSONObject("ctx2");
            }
            obj.put("actionContext", ctx.get("actionContext"));
        }

        List<NodeVO> nodes = ctx.getJSONArray("nodes").toJavaList(NodeVO.class);
        List<LinkVO> links = ctx.getJSONArray("links").toJavaList(LinkVO.class);
        obj.put("actionNodesIds", nodes.stream().map(NodeVO::getId).collect(Collectors.toList()));
        obj.put("actionLinksIds", links.stream().map(LinkVO::getId).collect(Collectors.toList()));
        if (!actionType.equals(ActionEnum.DELETE.label()) || isUndo) {
            obj.put("actionNodes", nodes);
            obj.put("actionLinks", links);
        }
        return obj;
    }

    public boolean graphFilterHandler(GraphActionDTO action, boolean isUndo) {
        GraphActionVO actionVO = action.view();
        JSONObject context = actionVO.getContextObj();
        JSONObject data = context.getJSONObject("data");
        GraphVO ctx1 = data.getObject("ctx1", GraphVO.class);
        GraphVO ctx2 = data.getObject("ctx2", GraphVO.class);
        GraphVO ctx;
        if (isUndo) {
            ctx = ctx1;
        } else {
            ctx = ctx2;
        }
        graphAnalysisService.updateFilterInstance(action.getGraphId(), ctx.getFilterInstanceId());
        return true;
    }
}
